package com.pocket.insight.base.mql.detector;

import com.pocket.insight.base.sql.Dialect;
import com.pocket.insight.controller.dto.DatasetResultDto;
import com.pocket.insight.entity.Dataset;
import com.pocket.insight.entity.Type;
import org.apache.commons.lang3.StringUtils;

import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
 * 描述：
 *
 * @author shuize
 * @create 2024/12/5
 */
public class TypeDetector {

    private Dialect               dialect;
    private HashMap<String, Long> timeConsumption = new HashMap<>();

    public TypeDetector(Dialect dialect) {
        this.dialect = dialect;
    }


    public void detectIf(DatasetResultDto resultDto, Predicate<Dataset.Field> needDetect) {
        if (resultDto == null || resultDto.fetchData() == null || resultDto.getSchema() == null || needDetect == null) {
            return;
        }
        List<Dataset.Field> schema = resultDto.getSchema();
        List<List<Object>> data = resultDto.fetchData();
        for (int i = 0; i < schema.size(); i++) {
            Dataset.Field field = schema.get(i);
            if (needDetect.test(field)) {
                detect(field, i, data);
                if (field.getOriginType() == Type.TypeName.string && field
                        .getGeneratedOptions().getSuggestedTypes().size() == 1) {
                    Object firstNotNull = null;
                    for (int j = 0; j < data.size(); j++) {
                        Object obj = ((List) data.get(j)).get(i);
                        if (isMeaningfulValue(obj)) {
                            firstNotNull = obj;
                            break;
                        }
                    }
                    if (firstNotNull == null) {
                        Type.FieldType numberType = new Type.FieldType();
                        numberType.type = Type.TypeName.number;
                        numberType.srcType = Type.TypeName.string;
                        field.getGeneratedOptions().getSuggestedTypes().add(numberType);
                    }
                }
            }
        }
    }

    public void detect(Dataset.Field field, final int columnIndex, final List<List<Object>> data) {
        detectOneColumn(field, new OneColumn() {
            public int getSize() {
                return data.size();
            }

            public Object get(int index) {
                return ((List) data.get(index)).get(columnIndex);
            }
        });
    }


    public void detectOneColumn(Dataset.Field field, OneColumn oneColumn) {
        Object firstNotNull = firstMeaningful(oneColumn);
        Type.TypeName originType = typeOfField(field);
        if (firstNotNull != null && originType != Type.TypeName.json) {
            originType = typeOfValue(firstNotNull);
        }
        TypeGuesser typeGuesser = new TypeGuesser(originType, oneColumn.getSize(), this.dialect);
        typeGuesser.seeding(firstNotNull);
        for (int i = 0; i < oneColumn.getSize(); i++) {
            Object obj = oneColumn.get(i);
            if (typeGuesser.potentialGuessers.isEmpty() && isMeaningfulValue(obj)) {
                typeGuesser.seeding(obj);
            }
            if (!typeGuesser.potentialGuessers.isEmpty() && !typeGuesser.input(obj)) {
                break;
            }
        }
        updateType(field, originType, typeGuesser);
    }


    private void updateType(Dataset.Field field, Type.TypeName originType, TypeGuesser typeGuesser) {
       // TODO
    }

    public static Type.TypeName typeOfValue(Object value) {
        for (Type type : Type.BASIC_TYPES) {
            if (type.valueSure(value)) {
                return type.getTypeName();
            }
        }
        return Type.TypeName.unknown;
    }

    public static Object firstMeaningful(OneColumn oneColumn) {
        for (int i = 0; i < oneColumn.getSize(); i++) {
            Object obj = oneColumn.get(i);
            if (isMeaningfulValue(obj)) {
                return obj;
            }
        }
        return null;
    }

    public static Object firstMeaningful(List<Object> data) {
        for (Object obj : data) {
            if (isMeaningfulValue(obj)) {
                return obj;
            }
        }
        return null;
    }

    private static boolean isMeaningfulValue(Object object) {
        return (object != null && (!(object instanceof String) || (!StringUtils.isBlank((String) object) && !"null".equalsIgnoreCase((String) object))));
    }

    public static Type.TypeName typeOfField(Dataset.Field field) {
        String nativeType = field.getGeneratedOptions().getNativeType();
        Integer sqlType = field.getGeneratedOptions().getSqlType();
        Type.TypeName type = typeOfFieldInSpecialCases(sqlType, nativeType);
        if (sqlType != null && type == Type.TypeName.unknown) {
            type = typeOfFieldBySqlType(sqlType);
        }
        return type;
    }

    public static Type.TypeName typeOfFieldInSpecialCases(Integer sqlType, String nativeType) {
        if (sqlType == null && "string".equals(nativeType)) {
            return Type.TypeName.string;
        }
        if (StringUtils.containsIgnoreCase(nativeType, "json")) {
            return Type.TypeName.json;
        }
        return Type.TypeName.unknown;
    }

    public static Type.TypeName typeOfFieldBySqlType(Integer sqlType) {
        switch (sqlType.intValue()) {
            case -7:
            case -6:
            case -5:
            case 4:
            case 5:
                return Type.TypeName.integer;
            case 2:
            case 3:
            case 6:
            case 7:
            case 8:
                return Type.TypeName.number;
            case -1:
            case 1:
            case 12:
                return Type.TypeName.string;
            case 91:
                return Type.TypeName.date;
            case 93:
                return Type.TypeName.time;
            case 2003:
                return Type.TypeName.array;
            case -4:
            case -3:
            case -2:
            case 0:
            case 70:
            case 1111:
            case 2000:
            case 2001:
            case 2002:
            case 2004:
            case 2005:
            case 2006:
                return Type.TypeName.unknown;
            case 16:
                return Type.TypeName.bool;
            case -8:
                return Type.TypeName.integer;
            case -16:
            case -15:
            case -9:
                return Type.TypeName.string;
            case 2009:
            case 2011:
            case 2012:
                return Type.TypeName.unknown;
            case 2013:
            case 2014:
                return Type.TypeName.time;
        }
        return Type.TypeName.unknown;
    }


    public static interface OneColumn {
        int getSize();

        Object get(int param1Int);
    }

    public static class TypeGuesser extends Type.FieldType {
                Dialect dialect;
        private long    amount;
        private long              invalidFreq       = 0L;
        private long              validFreq         = 0L;
        private long              nullFreq          = 0L;
        private Object            seed;
        private HashSet<Object>   castSet           = new HashSet();
        private List<TypeGuesser> potentialGuessers = new ArrayList<>();

        TypeGuesser(Type.TypeName type, long amount, Dialect dialect) {
            this.type = type;
            this.amount = amount;
            this.dialect = dialect;
        }

        void seeding(Object seed) {
            this.seed = seed;
            if (this.seed == null) {
                return;
            }
            Set<Type.TypeName> ignoreTypes = (Set<Type.TypeName>) this.potentialGuessers.stream().map(g -> g.type).collect(Collectors.toSet());
            ignoreTypes.add(this.type);
            startSeeding(ignoreTypes);
        }

        private void startSeeding(Set<Type.TypeName> ignoreTypes) {
            Set<Type.TypeName> nextIgnoreTypes = new HashSet<>(ignoreTypes);
            for (Type type : Type.BASIC_TYPES) {
                Type.TypeConfig typeConfig = new Type.TypeConfig();
                typeConfig.dialectName = this.dialect.getClass().getSimpleName();
                if (type.getTypeName() == this.type || !type.valueMaybe(this.seed) || (
                        !this.dialect.isJsonAvailable() && type.getTypeName() == Type.TypeName.json)) {
                    continue;
                }
                if (!this.dialect.isBoolAvailable() && type.getTypeName() == Type.TypeName.bool) {
                    continue;
                }
                Object casted = type.cast(this.seed, typeConfig);
                if (casted == null || ignoreTypes.contains(type.getTypeName()) ||
                        !isSupportedType(type.getTypeName(), typeConfig)) {
                    continue;
                }
                TypeGuesser nextGuesser = new TypeGuesser(type.getTypeName(), this.amount, this.dialect);
                nextGuesser.type = type.getTypeName();
                nextGuesser.srcType = this.type;
                nextGuesser.config = typeConfig;
                nextGuesser.seed = casted;
                this.potentialGuessers.add(nextGuesser);
                nextIgnoreTypes.add(type.getTypeName());
            }


            for (TypeGuesser nextGuesser : this.potentialGuessers) {
                nextGuesser.startSeeding(nextIgnoreTypes);
            }
        }

        public boolean input(Object value) {
            return inputImpl(value, false);
        }

        public boolean inputImpl(Object value, boolean invalid) {
            if (this.potentialGuessers.isEmpty()) {
                return false;
            }
            Iterator<TypeGuesser> it = this.potentialGuessers.iterator();
            int sureAmount = 0;
            while (it.hasNext()) {
                TypeGuesser guesser = it.next();
                Object casted = null;
                if (invalid) {
                    guesser.invalidFreq++;
                } else if (TypeDetector.isMeaningfulValue(value)) {
                    casted = guesser.type.type().cast(value, guesser.config);
                    if (casted != null) {
                        guesser.validFreq++;
                        this.castSet.add(casted);
                    } else {
                        guesser.invalidFreq++;
                    }
                } else {
                    guesser.nullFreq++;
                }

                if (guesser.notPossible()) {
                    it.remove();
                    continue;
                }
                if (guesser.veryPossible()) {
                    sureAmount++;
                }
                guesser.inputImpl(casted, (casted == null && TypeDetector.isMeaningfulValue(value)));
            }
            if (sureAmount == this.potentialGuessers.size() && sureAmount > 0) {
                this.potentialGuessers.sort(Comparator.comparingLong(fieldTypeFreq -> fieldTypeFreq.validFreq - fieldTypeFreq.invalidFreq));
            }
            return (!this.potentialGuessers.isEmpty() && sureAmount != this.potentialGuessers.size());
        }

        boolean notPossible() {
            return (this.invalidFreq > 10L + this.validFreq || this.invalidFreq > (this.amount - this.nullFreq) / 2L);
        }

        boolean veryPossible() {
            return ((this.invalidFreq < 1L && this.validFreq > Math.min(this.amount / 2L, 10L) && this.castSet.size() >= 5) || this.validFreq > this.amount * 2L / 3L);
        }


        boolean acceptable() {
            return (veryPossible() || this.validFreq > this.invalidFreq * 10L);
        }

        boolean isSupportedType(Type.TypeName typeName, Type.TypeConfig typeConfig) {
            if (typeName != Type.TypeName.date && typeName != Type.TypeName.time) {
                return true;
            }
            return (typeConfig != null && (StringUtils.isBlank(typeConfig.dateFormat)|| null != this.dialect.jodaToNativeParseFormat(typeConfig.dateFormat)));
        }

        public List<Type.FieldType> getSuggestedTypes() {
            List<Type.FieldType> suggestedTypes = getSuggestedTypesImpl();
            if (this.type == Type.TypeName.number) {
                suggestedTypes.removeIf(f -> (f.type == Type.TypeName.string && f.srcType == Type.TypeName.integer));
            }
            if (this.type != Type.TypeName.unknown && this.type != Type.TypeName.string && suggestedTypes.stream().noneMatch(f -> (f.type == Type.TypeName.string))) {
                Type.FieldType fieldType = new Type.FieldType();
                fieldType.srcType = this.type;
                fieldType.type = Type.TypeName.string;
                suggestedTypes.add(fieldType);
            }
            return suggestedTypes;
        }

        public List<Type.FieldType> getSuggestedTypesImpl() {
            this.potentialGuessers.sort((left, right) -> {
                long leftF = left.validFreq - left.invalidFreq;

                long rightF = right.validFreq - right.invalidFreq;
                int cmp = (int) (leftF - rightF);
                if (cmp == 0) {
                    cmp = left.type.compareTo(right.type);
                }
                return cmp;
            });
            List<Type.FieldType> suggestedTypes = new ArrayList<>();
            for (TypeGuesser guesser : this.potentialGuessers) {
                if (guesser.acceptable()) {
                    Type.FieldType fieldType = new Type.FieldType();
                    fieldType.type = guesser.type;
                    fieldType.config = guesser.config;
                    fieldType.srcType = guesser.srcType;
                    suggestedTypes.add(fieldType);
                }
            }
            for (TypeGuesser guesser : this.potentialGuessers) {
                List<Type.FieldType> nextTypes = guesser.getSuggestedTypesImpl();
                for (Type.FieldType fieldType : nextTypes) {
                    if (suggestedTypes.stream().noneMatch(t -> (t.type == fieldType.type))) {
                        suggestedTypes.add(fieldType);
                    }
                }
            }
            return suggestedTypes;
        }
    }
}
